use config::ConfigError;
use glob::PatternError;
use proptest::prelude::*;
use rand::prelude::ThreadRng;
use rstest::rstest;

use crate::cli::Arguments;
use crate::tests::common::{option_string_strategy, with_cwd};
use crate::tests::fixtures::{
    get_logs_for_current_thread,
    init_logging,
    rand_string,
    rand_usize,
    random_connectors,
    random_missing_tile_policy,
    random_tile_selection_policy,
    random_tile_text,
    random_tile_type,
    random_vacancy_policy,
    test_data,
    RANDOM_STRINGS,
    RANDOM_USIZE,
    RANDOM_USIZES,
    TEMPORARY_DIRECTORY,
};
use crate::tiling::tiles::tile_list::TileList;
use crate::tiling::tiles::{Connectors, TileConnection};
use crate::tiling::{Direction, MissingTilePolicy, TileSelectionPolicy, VacancyPolicy};
use crate::{configuration, run, tiling, Error};

#[test]
fn test_configuration_load_is_public() {
    let _ = configuration::load(None, None);
}

#[test]
fn test_connectors_get_connector_is_public() {
    let connectors: Connectors = random_connectors();

    let _ = connectors.get_connector(Direction::North);
}

#[test]
fn test_connectors_set_connector_is_public() {
    let mut connectors: Connectors = random_connectors();

    connectors.set_connector(Direction::East, 0);
}

#[test]
fn test_tile_connectors_is_public() {
    let tile = random_tile_text(random_connectors());

    let _ = tile.connectors;
}

#[test]
fn test_tile_connection_select_tile_by_priority_is_public() {
    let tile_connection = TileConnection {
        tiles: vec![0],
        weights: vec![0.0],
    };

    let _ = tile_connection.select_tile_by_priority();
}

#[test]
fn test_tile_connection_select_tile_randomly_is_public() {
    let mut rng: ThreadRng = rand::rng();
    let tile_connection = TileConnection {
        tiles: vec![0],
        weights: vec![0.0],
    };

    let _ = tile_connection.select_tile_randomly(&mut rng);
}

#[test]
fn test_slot_grid_populate_simple_is_public() {
    let mut slot_grid = tiling::SlotGrid::new(
        rand_usize(1, 10),
        rand_usize(2, 5),
        TileList {
            tiles: vec![],
            horizontal_connector_names: vec!["".to_string()],
            vertical_connector_names: vec!["".to_string()],
            tile_type: random_tile_type(),
        },
        None,
        random_missing_tile_policy(),
        random_tile_selection_policy(),
        random_vacancy_policy(),
    )
    .expect("the number of rows, columns as well as the border tile ID should be valid");

    let _ = slot_grid.populate_simple();
}

#[test]
fn test_slot_grid_populate_is_public() {
    let mut slot_grid = tiling::SlotGrid::new(
        rand_usize(2, 4),
        rand_usize(1, 5),
        TileList {
            tiles: vec![],
            horizontal_connector_names: vec!["".to_string()],
            vertical_connector_names: vec!["".to_string()],
            tile_type: random_tile_type(),
        },
        None,
        random_missing_tile_policy(),
        random_tile_selection_policy(),
        random_vacancy_policy(),
    )
    .expect("the number of rows, columns as well as the border tile ID should be valid");

    let _ = slot_grid.populate();
}

#[rstest]
fn test_error_value_error_raw_initialization(
    #[values("", " ", "₪", "ita", "call")] message: String,
) {
    let error = Error::ValueError(message.clone());

    if let Error::ValueError(msg) = error {
        assert_eq!(msg, message);
    } else {
        std::panic!("expected the ValueError variant, got something else");
    }
}

#[rstest]
fn test_error_io_error_raw_initialization(#[values("", " ", "e", "lam", "ola")] message: String) {
    let error = Error::IoError(message.clone());

    if let Error::IoError(msg) = error {
        assert_eq!(msg, message);
    } else {
        std::panic!("expected the IoError variant, got something else");
    }
}

#[test]
fn test_error_tile_not_found_error_raw_initialization() {
    let error = Error::TileNotFoundException(RANDOM_STRINGS[4].clone());

    if let Error::TileNotFoundException(msg) = error {
        assert_eq!(msg, RANDOM_STRINGS[4]);
    } else {
        std::panic!("expected the TileNotFoundException variant, got something else");
    }
}

#[test]
fn test_error_from_config_error() {
    let config_error = ConfigError::Message(RANDOM_STRINGS[0].clone());

    let error: Error = config_error.into();

    match error {
        Error::ValueError(error) => assert_eq!(RANDOM_STRINGS[0].clone(), error),
        _ => std::panic!("Expected ValueError variant, got something else"),
    }
}

#[test]
fn test_error_from_pattern_error() {
    let pattern_error = PatternError {
        pos: *RANDOM_USIZE,
        msg: &RANDOM_STRINGS[1],
    };

    let error: Error = pattern_error.into();

    match error {
        Error::ValueError(error) => assert_eq!(
            format!(
                "Pattern syntax error near position {}: {}",
                *RANDOM_USIZE,
                RANDOM_STRINGS[1].clone()
            ),
            error
        ),
        _ => std::panic!("Expected ValueError variant, got something else"),
    }
}

fn missing_tile_policy_strategy() -> impl Strategy<Value = MissingTilePolicy> {
    prop_oneof![
        Just(MissingTilePolicy::Avoid),
        Just(MissingTilePolicy::Exit),
    ]
}

fn vacancy_policy_strategy() -> impl Strategy<Value = VacancyPolicy> {
    prop_oneof![
        Just(VacancyPolicy::Avoid),
        Just(VacancyPolicy::AvoidStrict),
        Just(VacancyPolicy::Ignore),
    ]
}

fn tile_selection_policy_strategy() -> impl Strategy<Value = TileSelectionPolicy> {
    prop_oneof![
        Just(TileSelectionPolicy::Random),
        Just(TileSelectionPolicy::Priority),
    ]
}

prop_compose! {
    fn arguments_strategy()(
        rows in 0u16..2048,
        cols in 0u16..2048,
        border in option_string_strategy(),
        config in option_string_strategy(),
        tile_directory in option_string_strategy(),
        missing_tile_policy in missing_tile_policy_strategy(),
        no_config in any::<bool>(),
        output in ".*",
        vacancy_policy in vacancy_policy_strategy(),
        tile_selection_policy in tile_selection_policy_strategy(),
    ) -> Arguments {
        Arguments {
            rows,
            cols,
            border,
            config,
            tile_directory,
            missing_tile_policy,
            no_config,
            output,
            vacancy_policy,
            tile_selection_policy,
        }
    }
}

proptest! {
    #![proptest_config(ProptestConfig::with_cases(16))]

    #[test]
    fn test_run_does_not_panic(args in arguments_strategy()) {
        let _ = run(args);
    }
}

#[rstest]
// region zero rows
#[case::zero_rows(
    Arguments {
        rows: 0,
        cols: RANDOM_USIZES[3] as u16,
        border: None,
        config: None,
        tile_directory: None,
        missing_tile_policy: random_missing_tile_policy(),
        no_config: false,
        output: rand_string(rand_usize(0, 1023)),
        vacancy_policy: random_vacancy_policy(),
        tile_selection_policy: random_tile_selection_policy(),
    },
    Err(Error::ValueError(
        format!(
            "num_rows and num_cols must be > 0, got num_rows = 0 and num_cols = {}",
            RANDOM_USIZES[3],
        )
    )),
    vec![
        format!(
            "image_tiler::configuration: INFO ⸙ No tile directory provided; assuming current \
            working directory {}",
            env!("CARGO_MANIFEST_DIR"),
        )
    ],
    "."
)]
// endregion
// region config nil_connector.yaml passed
#[case::config_nil_connector_yaml_passed(
    Arguments {
        rows: 1,
        cols: 1,
        border: None,
        config: Some(test_data("empty/nil_connector.yaml")),
        tile_directory: None,
        missing_tile_policy: MissingTilePolicy::Avoid,
        no_config: false,
        output: RANDOM_STRINGS[4].clone(),
        vacancy_policy: VacancyPolicy::Avoid,
        tile_selection_policy: TileSelectionPolicy::Priority,
    },
    Ok(()),
    vec![
        format!(
        "image_tiler::configuration: INFO ⸙ No tile directory provided; assuming configuration \
        file directory {}",
        test_data("empty"),
        ),
    ],
    TEMPORARY_DIRECTORY.path().to_string_lossy().to_string()
)]
fn test_run(
    #[case] args: Arguments,
    #[case] expected_result: Result<(), Error>,
    #[case] expected_log_messages: Vec<String>,
    #[case] cwd: String,
) {
    with_cwd(cwd, || {
        init_logging();
        let result = run(args.clone());

        let log_messages = get_logs_for_current_thread();
        assert_eq!(expected_log_messages, log_messages);

        assert_eq!(expected_result, result);
    })
}
